import { Box3, MathUtils, Matrix4, Matrix3, Ray, Vector3 } from 'three'

// module scope helper variables

const a = {
  c: null, // center
  u: [/* @__PURE__ */ new Vector3(), /* @__PURE__ */ new Vector3(), /* @__PURE__ */ new Vector3()], // basis vectors
  e: [], // half width
}

const b = {
  c: null, // center
  u: [/* @__PURE__ */ new Vector3(), /* @__PURE__ */ new Vector3(), /* @__PURE__ */ new Vector3()], // basis vectors
  e: [], // half width
}

const R = [[], [], []]
const AbsR = [[], [], []]
const t = []

const xAxis = /* @__PURE__ */ new Vector3()
const yAxis = /* @__PURE__ */ new Vector3()
const zAxis = /* @__PURE__ */ new Vector3()
const v1 = /* @__PURE__ */ new Vector3()
const size = /* @__PURE__ */ new Vector3()
const closestPoint = /* @__PURE__ */ new Vector3()
const rotationMatrix = /* @__PURE__ */ new Matrix3()
const aabb = /* @__PURE__ */ new Box3()
const matrix = /* @__PURE__ */ new Matrix4()
const inverse = /* @__PURE__ */ new Matrix4()
const localRay = /* @__PURE__ */ new Ray()

// OBB

class OBB {
  constructor(center = new Vector3(), halfSize = new Vector3(), rotation = new Matrix3()) {
    this.center = center
    this.halfSize = halfSize
    this.rotation = rotation
  }

  set(center, halfSize, rotation) {
    this.center = center
    this.halfSize = halfSize
    this.rotation = rotation

    return this
  }

  copy(obb) {
    this.center.copy(obb.center)
    this.halfSize.copy(obb.halfSize)
    this.rotation.copy(obb.rotation)

    return this
  }

  clone() {
    return new this.constructor().copy(this)
  }

  getSize(result) {
    return result.copy(this.halfSize).multiplyScalar(2)
  }

  /**
   * Reference: Closest Point on OBB to Point in Real-Time Collision Detection
   * by Christer Ericson (chapter 5.1.4)
   */
  clampPoint(point, result) {
    const halfSize = this.halfSize

    v1.subVectors(point, this.center)
    this.rotation.extractBasis(xAxis, yAxis, zAxis)

    // start at the center position of the OBB

    result.copy(this.center)

    // project the target onto the OBB axes and walk towards that point

    const x = MathUtils.clamp(v1.dot(xAxis), -halfSize.x, halfSize.x)
    result.add(xAxis.multiplyScalar(x))

    const y = MathUtils.clamp(v1.dot(yAxis), -halfSize.y, halfSize.y)
    result.add(yAxis.multiplyScalar(y))

    const z = MathUtils.clamp(v1.dot(zAxis), -halfSize.z, halfSize.z)
    result.add(zAxis.multiplyScalar(z))

    return result
  }

  containsPoint(point) {
    v1.subVectors(point, this.center)
    this.rotation.extractBasis(xAxis, yAxis, zAxis)

    // project v1 onto each axis and check if these points lie inside the OBB

    return (
      Math.abs(v1.dot(xAxis)) <= this.halfSize.x &&
      Math.abs(v1.dot(yAxis)) <= this.halfSize.y &&
      Math.abs(v1.dot(zAxis)) <= this.halfSize.z
    )
  }

  intersectsBox3(box3) {
    return this.intersectsOBB(obb.fromBox3(box3))
  }

  intersectsSphere(sphere) {
    // find the point on the OBB closest to the sphere center

    this.clampPoint(sphere.center, closestPoint)

    // if that point is inside the sphere, the OBB and sphere intersect

    return closestPoint.distanceToSquared(sphere.center) <= sphere.radius * sphere.radius
  }

  /**
   * Reference: OBB-OBB Intersection in Real-Time Collision Detection
   * by Christer Ericson (chapter 4.4.1)
   *
   */
  intersectsOBB(obb, epsilon = Number.EPSILON) {
    // prepare data structures (the code uses the same nomenclature like the reference)

    a.c = this.center
    a.e[0] = this.halfSize.x
    a.e[1] = this.halfSize.y
    a.e[2] = this.halfSize.z
    this.rotation.extractBasis(a.u[0], a.u[1], a.u[2])

    b.c = obb.center
    b.e[0] = obb.halfSize.x
    b.e[1] = obb.halfSize.y
    b.e[2] = obb.halfSize.z
    obb.rotation.extractBasis(b.u[0], b.u[1], b.u[2])

    // compute rotation matrix expressing b in a's coordinate frame

    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        R[i][j] = a.u[i].dot(b.u[j])
      }
    }

    // compute translation vector

    v1.subVectors(b.c, a.c)

    // bring translation into a's coordinate frame

    t[0] = v1.dot(a.u[0])
    t[1] = v1.dot(a.u[1])
    t[2] = v1.dot(a.u[2])

    // compute common subexpressions. Add in an epsilon term to
    // counteract arithmetic errors when two edges are parallel and
    // their cross product is (near) null

    for (let i = 0; i < 3; i++) {
      for (let j = 0; j < 3; j++) {
        AbsR[i][j] = Math.abs(R[i][j]) + epsilon
      }
    }

    let ra, rb

    // test axes L = A0, L = A1, L = A2

    for (let i = 0; i < 3; i++) {
      ra = a.e[i]
      rb = b.e[0] * AbsR[i][0] + b.e[1] * AbsR[i][1] + b.e[2] * AbsR[i][2]
      if (Math.abs(t[i]) > ra + rb) return false
    }

    // test axes L = B0, L = B1, L = B2

    for (let i = 0; i < 3; i++) {
      ra = a.e[0] * AbsR[0][i] + a.e[1] * AbsR[1][i] + a.e[2] * AbsR[2][i]
      rb = b.e[i]
      if (Math.abs(t[0] * R[0][i] + t[1] * R[1][i] + t[2] * R[2][i]) > ra + rb) return false
    }

    // test axis L = A0 x B0

    ra = a.e[1] * AbsR[2][0] + a.e[2] * AbsR[1][0]
    rb = b.e[1] * AbsR[0][2] + b.e[2] * AbsR[0][1]
    if (Math.abs(t[2] * R[1][0] - t[1] * R[2][0]) > ra + rb) return false

    // test axis L = A0 x B1

    ra = a.e[1] * AbsR[2][1] + a.e[2] * AbsR[1][1]
    rb = b.e[0] * AbsR[0][2] + b.e[2] * AbsR[0][0]
    if (Math.abs(t[2] * R[1][1] - t[1] * R[2][1]) > ra + rb) return false

    // test axis L = A0 x B2

    ra = a.e[1] * AbsR[2][2] + a.e[2] * AbsR[1][2]
    rb = b.e[0] * AbsR[0][1] + b.e[1] * AbsR[0][0]
    if (Math.abs(t[2] * R[1][2] - t[1] * R[2][2]) > ra + rb) return false

    // test axis L = A1 x B0

    ra = a.e[0] * AbsR[2][0] + a.e[2] * AbsR[0][0]
    rb = b.e[1] * AbsR[1][2] + b.e[2] * AbsR[1][1]
    if (Math.abs(t[0] * R[2][0] - t[2] * R[0][0]) > ra + rb) return false

    // test axis L = A1 x B1

    ra = a.e[0] * AbsR[2][1] + a.e[2] * AbsR[0][1]
    rb = b.e[0] * AbsR[1][2] + b.e[2] * AbsR[1][0]
    if (Math.abs(t[0] * R[2][1] - t[2] * R[0][1]) > ra + rb) return false

    // test axis L = A1 x B2

    ra = a.e[0] * AbsR[2][2] + a.e[2] * AbsR[0][2]
    rb = b.e[0] * AbsR[1][1] + b.e[1] * AbsR[1][0]
    if (Math.abs(t[0] * R[2][2] - t[2] * R[0][2]) > ra + rb) return false

    // test axis L = A2 x B0

    ra = a.e[0] * AbsR[1][0] + a.e[1] * AbsR[0][0]
    rb = b.e[1] * AbsR[2][2] + b.e[2] * AbsR[2][1]
    if (Math.abs(t[1] * R[0][0] - t[0] * R[1][0]) > ra + rb) return false

    // test axis L = A2 x B1

    ra = a.e[0] * AbsR[1][1] + a.e[1] * AbsR[0][1]
    rb = b.e[0] * AbsR[2][2] + b.e[2] * AbsR[2][0]
    if (Math.abs(t[1] * R[0][1] - t[0] * R[1][1]) > ra + rb) return false

    // test axis L = A2 x B2

    ra = a.e[0] * AbsR[1][2] + a.e[1] * AbsR[0][2]
    rb = b.e[0] * AbsR[2][1] + b.e[1] * AbsR[2][0]
    if (Math.abs(t[1] * R[0][2] - t[0] * R[1][2]) > ra + rb) return false

    // since no separating axis is found, the OBBs must be intersecting

    return true
  }

  /**
   * Reference: Testing Box Against Plane in Real-Time Collision Detection
   * by Christer Ericson (chapter 5.2.3)
   */
  intersectsPlane(plane) {
    this.rotation.extractBasis(xAxis, yAxis, zAxis)

    // compute the projection interval radius of this OBB onto L(t) = this->center + t * p.normal;

    const r =
      this.halfSize.x * Math.abs(plane.normal.dot(xAxis)) +
      this.halfSize.y * Math.abs(plane.normal.dot(yAxis)) +
      this.halfSize.z * Math.abs(plane.normal.dot(zAxis))

    // compute distance of the OBB's center from the plane

    const d = plane.normal.dot(this.center) - plane.constant

    // Intersection occurs when distance d falls within [-r,+r] interval

    return Math.abs(d) <= r
  }

  /**
   * Performs a ray/OBB intersection test and stores the intersection point
   * to the given 3D vector. If no intersection is detected, *null* is returned.
   */
  intersectRay(ray, result) {
    // the idea is to perform the intersection test in the local space
    // of the OBB.

    this.getSize(size)
    aabb.setFromCenterAndSize(v1.set(0, 0, 0), size)

    // create a 4x4 transformation matrix

    matrix.setFromMatrix3(this.rotation)
    matrix.setPosition(this.center)

    // transform ray to the local space of the OBB

    inverse.copy(matrix).invert()
    localRay.copy(ray).applyMatrix4(inverse)

    // perform ray <-> AABB intersection test

    if (localRay.intersectBox(aabb, result)) {
      // transform the intersection point back to world space

      return result.applyMatrix4(matrix)
    } else {
      return null
    }
  }

  /**
   * Performs a ray/OBB intersection test. Returns either true or false if
   * there is a intersection or not.
   */
  intersectsRay(ray) {
    return this.intersectRay(ray, v1) !== null
  }

  fromBox3(box3) {
    box3.getCenter(this.center)

    box3.getSize(this.halfSize).multiplyScalar(0.5)

    this.rotation.identity()

    return this
  }

  equals(obb) {
    return obb.center.equals(this.center) && obb.halfSize.equals(this.halfSize) && obb.rotation.equals(this.rotation)
  }

  applyMatrix4(matrix) {
    const e = matrix.elements

    let sx = v1.set(e[0], e[1], e[2]).length()
    const sy = v1.set(e[4], e[5], e[6]).length()
    const sz = v1.set(e[8], e[9], e[10]).length()

    const det = matrix.determinant()
    if (det < 0) sx = -sx

    rotationMatrix.setFromMatrix4(matrix)

    const invSX = 1 / sx
    const invSY = 1 / sy
    const invSZ = 1 / sz

    rotationMatrix.elements[0] *= invSX
    rotationMatrix.elements[1] *= invSX
    rotationMatrix.elements[2] *= invSX

    rotationMatrix.elements[3] *= invSY
    rotationMatrix.elements[4] *= invSY
    rotationMatrix.elements[5] *= invSY

    rotationMatrix.elements[6] *= invSZ
    rotationMatrix.elements[7] *= invSZ
    rotationMatrix.elements[8] *= invSZ

    this.rotation.multiply(rotationMatrix)

    this.halfSize.x *= sx
    this.halfSize.y *= sy
    this.halfSize.z *= sz

    v1.setFromMatrixPosition(matrix)
    this.center.add(v1)

    return this
  }
}

const obb = /* @__PURE__ */ new OBB()

export { OBB }
